/**
 * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @fileOverview Image plugin based on Widgets API
 */

'use strict';

CKEDITOR.dialog.add('image2', function(editor) {

    // RegExp: 123, 123px, empty string ""
    var regexGetSizeOrEmpty = /(^\s*(\d+)(px)?\s*$)|^$/i,

        lockButtonId = CKEDITOR.tools.getNextId(),
        resetButtonId = CKEDITOR.tools.getNextId(),

        lang = editor.lang.image2,
        commonLang = editor.lang.common,

        lockResetStyle = 'margin-top:18px;width:40px;height:20px;',
        lockResetHtml = new CKEDITOR.template(
            '<div>' +
            '<a href="javascript:void(0)" tabindex="-1" title="' + lang.lockRatio + '" class="cke_btn_locked" id="{lockButtonId}" role="checkbox">' +
            '<span class="cke_icon"></span>' +
            '<span class="cke_label">' + lang.lockRatio + '</span>' +
            '</a>' +

            '<a href="javascript:void(0)" tabindex="-1" title="' + lang.resetSize + '" class="cke_btn_reset" id="{resetButtonId}" role="button">' +
            '<span class="cke_label">' + lang.resetSize + '</span>' +
            '</a>' +
            '</div>').output({
            lockButtonId: lockButtonId,
            resetButtonId: resetButtonId
        }),

        helpers = CKEDITOR.plugins.image2,

        // Editor instance configuration.
        config = editor.config,

        hasFileBrowser = !!(config.filebrowserImageBrowseUrl || config.filebrowserBrowseUrl),

        // Content restrictions defined by the widget which
        // impact on dialog structure and presence of fields.
        features = editor.widgets.registered.image.features,

        // Functions inherited from image2 plugin.
        getNatural = helpers.getNatural,

        // Global variables referring to the dialog's context.
        doc, widget, image,

        // Global variable referring to this dialog's image pre-loader.
        preLoader,

        // Global variables holding the original size of the image.
        domWidth, domHeight,

        // Global variables related to image pre-loading.
        preLoadedWidth, preLoadedHeight, srcChanged,

        // Global variables related to size locking.
        lockRatio, userDefinedLock,

        // Global variables referring to dialog fields and elements.
        lockButton, resetButton, widthField, heightField,

        natural;

    // Validates dimension. Allowed values are:
    // "123px", "123", "" (empty string)
    function validateDimension() {
        var match = this.getValue().match(regexGetSizeOrEmpty),
            isValid = !!(match && parseInt(match[1], 10) !== 0);

        if (!isValid)
            alert(commonLang['invalidLength'].replace('%1', commonLang[this.id]).replace('%2', 'px')); // jshint ignore:line

        return isValid;
    }

    // Creates a function that pre-loads images. The callback function passes
    // [image, width, height] or null if loading failed.
    //
    // @returns {Function}
    function createPreLoader() {
        var image = doc.createElement('img'),
            listeners = [];

        function addListener(event, callback) {
            listeners.push(image.once(event, function(evt) {
                removeListeners();
                callback(evt);
            }));
        }

        function removeListeners() {
            var l;

            while ((l = listeners.pop()))
                l.removeListener();
        }

        // @param {String} src.
        // @param {Function} callback.
        return function(src, callback, scope) {
            addListener('load', function() {
                // Don't use image.$.(width|height) since it's buggy in IE9-10 (https://dev.ckeditor.com/ticket/11159)
                var dimensions = getNatural(image);

                callback.call(scope, image, dimensions.width, dimensions.height);
            });

            addListener('error', function() {
                callback(null);
            });

            addListener('abort', function() {
                callback(null);
            });

            image.setAttribute('src',
                (config.baseHref || '') + src + '?' + Math.random().toString(16).substring(2));
        };
    }

    // This function updates width and height fields once the
    // "src" field is altered. Along with dimensions, also the
    // dimensions lock is adjusted.
    function onChangeSrc() {
        var value = this.getValue();

        toggleDimensions(false);

        // Remember that src is different than default.
        if (value !== widget.data.src) {
            // Update dimensions of the image once it's preloaded.
            preLoader(value, function(image, width, height) {
                // Re-enable width and height fields.
                toggleDimensions(true);

                // There was problem loading the image. Unlock ratio.
                if (!image)
                    return toggleLockRatio(false);

                // Fill width field with the width of the new image.
                widthField.setValue(editor.config.image2_prefillDimensions === false ? 0 : width);

                // Fill height field with the height of the new image.
                heightField.setValue(editor.config.image2_prefillDimensions === false ? 0 : height);

                // Cache the new width and update initial cache (#1348).
                preLoadedWidth = domWidth = width;

                // Cache the new height and update initial cache (#1348).
                preLoadedHeight = domHeight = height;

                // Check for new lock value if image exist.
                toggleLockRatio(helpers.checkHasNaturalRatio(image));
            });

            srcChanged = true;
        }

        // Value is the same as in widget data but is was
        // modified back in time. Roll back dimensions when restoring
        // default src.
        else if (srcChanged) {
            // Re-enable width and height fields.
            toggleDimensions(true);

            // Restore width field with cached width.
            widthField.setValue(domWidth);

            // Restore height field with cached height.
            heightField.setValue(domHeight);

            // Src equals default one back again.
            srcChanged = false;
        }

        // Value is the same as in widget data and it hadn't
        // been modified.
        else {
            // Re-enable width and height fields.
            toggleDimensions(true);
        }
    }

    function onChangeDimension() {
        // If ratio is un-locked, then we don't care what's next.
        if (!lockRatio)
            return;

        var value = this.getValue();

        // No reason to auto-scale or unlock if the field is empty.
        if (!value)
            return;

        // If the value of the field is invalid (e.g. with %), unlock ratio.
        if (!value.match(regexGetSizeOrEmpty))
            toggleLockRatio(false);

        // No automatic re-scale when dimension is '0'.
        if (value === '0')
            return;

        var isWidth = this.id == 'width',
            // If dialog opened for the new image, domWidth and domHeight
            // will be empty. Use dimensions from pre-loader in such case instead.
            width = domWidth || preLoadedWidth,
            height = domHeight || preLoadedHeight;

        // If changing width, then auto-scale height.
        if (isWidth)
            value = Math.round(height * (value / width));

        // If changing height, then auto-scale width.
        else
            value = Math.round(width * (value / height));

        // If the value is a number, apply it to the other field.
        if (!isNaN(value))
            (isWidth ? heightField : widthField).setValue(value);
    }

    // Set-up function for lock and reset buttons:
    // 	* Adds lock and reset buttons to focusables. Check if button exist first
    // 	  because it may be disabled e.g. due to ACF restrictions.
    // 	* Register mouseover and mouseout event listeners for UI manipulations.
    // 	* Register click event listeners for buttons.
    function onLoadLockReset() {
        var dialog = this.getDialog();

        function setupMouseClasses(el) {
            el.on('mouseover', function() {
                this.addClass('cke_btn_over');
            }, el);

            el.on('mouseout', function() {
                this.removeClass('cke_btn_over');
            }, el);
        }

        // Create references to lock and reset buttons for this dialog instance.
        lockButton = doc.getById(lockButtonId);
        resetButton = doc.getById(resetButtonId);

        // Activate (Un)LockRatio button
        if (lockButton) {
            // Consider that there's an additional focusable field
            // in the dialog when the "browse" button is visible.
            dialog.addFocusable(lockButton, 4 + hasFileBrowser);

            lockButton.on('click', function(evt) {
                toggleLockRatio();
                evt.data && evt.data.preventDefault();
            }, this.getDialog());

            setupMouseClasses(lockButton);
        }

        // Activate the reset size button.
        if (resetButton) {
            // Consider that there's an additional focusable field
            // in the dialog when the "browse" button is visible.
            dialog.addFocusable(resetButton, 5 + hasFileBrowser);

            // Fills width and height fields with the original dimensions of the
            // image (stored in widget#data since widget#init).
            resetButton.on('click', function(evt) {
                // If there's a new image loaded, reset button should revert
                // cached dimensions of pre-loaded DOM element.
                if (srcChanged) {
                    widthField.setValue(preLoadedWidth);
                    heightField.setValue(preLoadedHeight);
                }

                // If the old image remains, reset button should revert
                // dimensions as loaded when the dialog was first shown.
                else {
                    widthField.setValue(domWidth);
                    heightField.setValue(domHeight);
                }

                evt.data && evt.data.preventDefault();
            }, this);

            setupMouseClasses(resetButton);
        }
    }

    function toggleLockRatio(enable) {
        // No locking if there's no radio (i.e. due to ACF).
        if (!lockButton)
            return;

        if (typeof enable == 'boolean') {
            // If user explicitly wants to decide whether
            // to lock or not, don't do anything.
            if (userDefinedLock)
                return;

            lockRatio = enable;
        }

        // Undefined. User changed lock value.
        else {
            var width = widthField.getValue(),
                height;

            userDefinedLock = true;
            lockRatio = !lockRatio;

            // Automatically adjust height to width to match
            // the original ratio (based on dom- dimensions).
            if (lockRatio && width) {
                height = domHeight / domWidth * width;

                if (!isNaN(height))
                    heightField.setValue(Math.round(height));
            }
        }

        lockButton[lockRatio ? 'removeClass' : 'addClass']('cke_btn_unlocked');
        lockButton.setAttribute('aria-checked', lockRatio);

        // Ratio button hc presentation - WHITE SQUARE / BLACK SQUARE
        if (CKEDITOR.env.hc) {
            var icon = lockButton.getChild(0);
            icon.setHtml(lockRatio ? CKEDITOR.env.ie ? '\u25A0' : '\u25A3' : CKEDITOR.env.ie ? '\u25A1' : '\u25A2');
        }
    }

    function toggleDimensions(enable) {
        var method = enable ? 'enable' : 'disable';

        widthField[method]();
        heightField[method]();
    }

    var srcBoxChildren = [{
        id: 'src',
        type: 'text',
        label: commonLang.url,
        onKeyup: onChangeSrc,
        onChange: onChangeSrc,
        setup: function(widget) {
            this.setValue(widget.data.src);
        },
        commit: function(widget) {
            widget.setData('src', this.getValue());
        },
        validate: CKEDITOR.dialog.validate.notEmpty(lang.urlMissing)
    }];

    // Render the "Browse" button on demand to avoid an "empty" (hidden child)
    // space in dialog layout that distorts the UI.
    if (hasFileBrowser) {
        srcBoxChildren.push({
            type: 'button',
            id: 'browse',
            // v-align with the 'txtUrl' field.
            // TODO: We need something better than a fixed size here.
            style: 'display:inline-block;margin-top:14px;',
            align: 'center',
            label: editor.lang.common.browseServer,
            hidden: true,
            filebrowser: 'info:src'
        });
    }

    return {
        title: lang.title,
        minWidth: 250,
        minHeight: 100,
        resizable: CKEDITOR.DIALOG_RESIZE_NONE,
        onLoad: function() {
            // Create a "global" reference to the document for this dialog instance.
            doc = this._.element.getDocument();

            // Create a pre-loader used for determining dimensions of new images.
            preLoader = createPreLoader();
        },
        onShow: function() {
            // Create a "global" reference to edited widget.
            widget = this.widget;

            // Create a "global" reference to widget's image.
            image = widget.parts.image;

            // Reset global variables.
            srcChanged = userDefinedLock = lockRatio = false;

            // Natural dimensions of the image.
            natural = getNatural(image);

            // Get the natural width of the image.
            preLoadedWidth = domWidth = natural.width;

            // Get the natural height of the image.
            preLoadedHeight = domHeight = natural.height;
        },
        contents: [{
                id: 'info',
                label: lang.infoTab,
                elements: [{
                        type: 'vbox',
                        padding: 0,
                        children: [{
                            type: 'hbox',
                            widths: ['100%'],
                            className: 'cke_dialog_image_url',
                            children: srcBoxChildren
                        }]
                    },
                    {
                        id: 'alt',
                        type: 'text',
                        label: lang.alt,
                        setup: function(widget) {
                            this.setValue(widget.data.alt);
                        },
                        commit: function(widget) {
                            widget.setData('alt', this.getValue());
                        },
                        validate: editor.config.image2_altRequired === true ? CKEDITOR.dialog.validate.notEmpty(lang.altMissing) : null
                    },
                    {
                        type: 'hbox',
                        widths: ['25%', '25%', '50%'],
                        requiredContent: features.dimension.requiredContent,
                        children: [{
                                type: 'text',
                                width: '45px',
                                id: 'width',
                                label: commonLang.width,
                                validate: validateDimension,
                                onKeyUp: onChangeDimension,
                                onLoad: function() {
                                    widthField = this;
                                },
                                setup: function(widget) {
                                    this.setValue(widget.data.width);
                                },
                                commit: function(widget) {
                                    widget.setData('width', this.getValue());
                                }
                            },
                            {
                                type: 'text',
                                id: 'height',
                                width: '45px',
                                label: commonLang.height,
                                validate: validateDimension,
                                onKeyUp: onChangeDimension,
                                onLoad: function() {
                                    heightField = this;
                                },
                                setup: function(widget) {
                                    this.setValue(widget.data.height);
                                },
                                commit: function(widget) {
                                    widget.setData('height', this.getValue());
                                }
                            },
                            {
                                id: 'lock',
                                type: 'html',
                                style: lockResetStyle,
                                onLoad: onLoadLockReset,
                                setup: function(widget) {
                                    toggleLockRatio(widget.data.lock);
                                },
                                commit: function(widget) {
                                    widget.setData('lock', lockRatio);
                                },
                                html: lockResetHtml
                            }
                        ]
                    },
                    {
                        type: 'hbox',
                        id: 'alignment',
                        requiredContent: features.align.requiredContent,
                        children: [{
                            id: 'align',
                            type: 'radio',
                            items: [
                                [commonLang.alignNone, 'none'],
                                [commonLang.left, 'left'],
                                [commonLang.center, 'center'],
                                [commonLang.right, 'right']
                            ],
                            label: commonLang.align,
                            setup: function(widget) {
                                this.setValue(widget.data.align);
                            },
                            commit: function(widget) {
                                widget.setData('align', this.getValue());
                            }
                        }]
                    },
                    {
                        id: 'hasCaption',
                        type: 'checkbox',
                        label: lang.captioned,
                        requiredContent: features.caption.requiredContent,
                        setup: function(widget) {
                            this.setValue(widget.data.hasCaption);
                        },
                        commit: function(widget) {
                            widget.setData('hasCaption', this.getValue());
                        }
                    }
                ]
            },
            {
                id: 'Upload',
                hidden: true,
                filebrowser: 'uploadButton',
                label: lang.uploadTab,
                elements: [{
                        type: 'file',
                        id: 'upload',
                        label: lang.btnUpload,
                        style: 'height:40px'
                    },
                    {
                        type: 'fileButton',
                        id: 'uploadButton',
                        filebrowser: 'info:src',
                        label: lang.btnUpload,
                        'for': ['Upload', 'upload']
                    }
                ]
            }
        ]
    };
});